#include "viewer.h"

#include "xfunc.widget.h"
#include "color.widget.h"
#include "misc.h"
#include "tracer.h"
#include "load.thread.h"
#include "options.h"
//#include "options.page.h"

#include <QtWidgets/QtWidgets>
#include <QOpenGLFunctions_3_3_Core>
#include <cmath>
#include <functional>
#include "viewershared.h"
#include "viewerpriv.h"

#define MAX_SLICES 480

#define PICK_SIZE 4


#include "viewer.options.h"


ViewerShared* Viewer::createShared() {
	return new ViewerShared;
}
void Viewer::destroyShared(ViewerShared* viewerShared) {
	delete viewerShared;
}


Viewer::Viewer(Session* s):
	QOpenGLWidget{s, 0},
	priv{new ViewerPriv{s, this}}
{
	printMessage("Viewer new: ", priv->session, ", ", this);
	setMinimumSize(QSize(480, 320));
	setFocusPolicy(Qt::WheelFocus);
#ifdef DEBUG_FPS
	QTimer *timer;
	timer=new QTimer{this};
	timer->start(10);
	connect(timer, &QTimer::timeout, this, static_cast<void (Viewer::*)()>(&Viewer::repaint));
#endif

	setAcceptDrops(true);

	initPainter();
}
Viewer::~Viewer() {
	delete m_Painter;
	printMessage("Viewer destroy: ", priv->session, ", ", this);
	delete priv;
	priv = nullptr;
}

void Viewer::setSession(Options* options, Graph gr, const std::array<Cube, 9>& cubes, const std::array<std::pair<double, double>, 9>& xfuncs, const std::array<double, 6>& bbox, double mvol) {
	if(!priv->funcs)
		return;
	priv->setSession(options, gr, cubes, xfuncs, bbox, mvol);
}

void Viewer::initializeGL() {
	bool fail=false;
	try {
		priv->initializeGL();
	} catch(const std::exception& e) {
		showWarning("Failed to initializeGL", e.what(), priv->session);
		fail=true;
	}
	priv->session->viewerReady(fail);
}

void Viewer::paintGL() {
	if(!priv->funcs)
		return;

	if(priv->graph) {
		priv->paintEdge();

		priv->paintOpaque();
		priv->paintRuler();
		priv->paintText(m_textImg, m_textUpdate);
		priv->paintSurface();
		priv->paintVolume();
		priv->paintFinish(defaultFramebufferObject());
		m_textUpdate = false;
	} else {
		priv->paintBlank();
	}
#ifdef DEBUG_FPS
	gl(Finish, priv->funcs)();
	printFPS();
#endif


	//void * ptr = (void*)m_textImg->bits();
	
}

void Viewer::resizeGL(int w, int h) {
	if(!priv->funcs)
		return;
	auto dpr=devicePixelRatio();
	int ww=dpr*w;
	int hh=dpr*h;
	initPainter();
	paintText("Fast Neurite Tracer", 1, 20);
	return priv->resizeFrameBuffers(ww, hh);
}

void Viewer::wheelEvent(QWheelEvent* event) {
	if(priv->graph) {
		float d=event->angleDelta().y()/120.0;
		priv->zoomInOut(d);
	}
}

void Viewer::mousePressEvent(QMouseEvent* ev) {
	auto x=ev->x();
	auto y=ev->y();
	priv->mouse.xPressed=x;
	priv->mouse.yPressed=y;
	priv->mouse.x=x;
	priv->mouse.y=y;
	priv->mouse.moved=false;
}

void Viewer::mouseReleaseEvent(QMouseEvent* ev) {
	auto x=ev->x();
	auto y=ev->y();
	if(priv->mouse.x!=x || priv->mouse.y!=y) {
		priv->mouse.x=x;
		priv->mouse.y=y;
		priv->mouse.moved=true;
	}
	if(!priv->mouse.moved) {
		switch(ev->button()) {
			case Qt::LeftButton:
				if(priv->graph) {
					auto dpr=devicePixelRatio();
					if(priv->pickPoint(x*dpr, y*dpr)) {
						Tracer::instance()->logMessage("STAT: ", "EDIT_PICK_POS");
					}
					update();
				}
				break;
			case Qt::RightButton:
				priv->session->showPopupMenu(ev->globalPos());
				break;
			default:
				break;
		}
	}
}
void Viewer::mouseMoveEvent(QMouseEvent* ev) {
	auto x=ev->x();
	auto y=ev->y();
	if(priv->mouse.x!=x || priv->mouse.y!=y) {
		auto old_x=priv->mouse.x;
		auto old_y=priv->mouse.y;
		priv->mouse.x=x;
		priv->mouse.y=y;
		priv->mouse.moved=true;
		if(priv->graph && ev->buttons()==Qt::LeftButton) {
			QVector3D a((old_x*2+1.0)/width()-1, 1-(old_y*2+1.0)/height(), -1);
			QVector3D b((x*2+1.0)/width()-1,1-(y*2+1.0)/height(), -1);
			a=(priv->mrView*priv->mrProj*a);
			a.normalize();
			b=(priv->mrView*priv->mrProj*b);
			b.normalize();
			QVector3D norm=QVector3D::crossProduct(a, b);
			QMatrix4x4 mat{};
			float proj=QVector3D::dotProduct(a, b);
			if(proj<-1) proj=-1;
			if(proj>1) proj=1;
			float r=360*acos(proj)/2/M_PI;
			mat.rotate(-r, norm);
			priv->up=mat*priv->up;
			priv->rgt=mat*priv->rgt;
			priv->updateMVPMatrices();
			update();
		}
	}
}
bool Viewer::clearSelection() {
	priv->pickA=priv->pickB=QVector3D();
	return true;
}
void Viewer::updateCube(int ch, const Cube& cube, const std::pair<double, double>& xfunc) {
	// min voxel size
	makeCurrent();
	if(priv->cube_textures[ch]) {
		priv->viewerShared->releaseTexture(priv->funcs, priv->cube_textures[ch]);
		priv->cube_textures[ch]=0;
	}
	priv->cubes[ch]=cube;
	priv->xfuncs[ch]=xfunc;
	if(cube.data) {
		priv->cube_textures[ch]=priv->viewerShared->getTexture(priv->funcs, priv->cubes[ch].data);
		gl(BindBuffer, priv->funcs)(GL_ARRAY_BUFFER, priv->pathBuffers[priv->pbiFixed].vbo);

		auto ptr=static_cast<PointGL*>(priv->funcs->glMapBufferRange(GL_ARRAY_BUFFER, (2+ch*24)*sizeof(PointGL), 24*sizeof(PointGL), GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_RANGE_BIT));
		priv->setAxes(cube, ptr);
		gl(UnmapBuffer, priv->funcs)(GL_ARRAY_BUFFER);
		priv->updateCubeMatrix(ch);
		priv->updateVoxelSizes(ch);
	}
	doneCurrent();
}
void Viewer::updatePosition(const Position& pos, bool cur) {
	if(cur) {
		priv->curPos=pos;
		for(int i=0; i<9; i++) {
			if(priv->cubes[i].data)
				priv->updateCubeMatrix(i);
		}
	} else {
		priv->tgtPos=pos;
	}
}
void Viewer::updateEdgeNode(EdgePriv* ep, size_t edgPos) {
	if(ep->vaoi<=0) {
		return updateEdge(ep);
	}
	makeCurrent();
	gl(BindBuffer, priv->funcs)(GL_ARRAY_BUFFER, priv->pathBuffers[ep->vaoi].vbo);
	auto ptr=static_cast<PointGL*>(priv->funcs->glMapBufferRange(GL_ARRAY_BUFFER, edgPos*sizeof(PointGL), 1*sizeof(PointGL), GL_MAP_WRITE_BIT));
	ptr->r=ep->points[edgPos]._r;
	ptr->m=ep->points[edgPos].m;
	gl(UnmapBuffer, priv->funcs)(GL_ARRAY_BUFFER);
	doneCurrent();
}
void Viewer::recycleEdge(EdgePriv* ep) {
	if(ep->vaoi<=0)
		return;
	if (priv) {
		priv->freePbi(ep->vaoi);
		ep->vaoi = -1;
	}
}
void Viewer::updateEdge(EdgePriv* ep) {
	makeCurrent();
	if(ep->vaoi<=0)
		ep->vaoi=priv->createPbi();
	auto pathGL=priv->pathToPathGL(ep->points, ep->parentVertex?ep->parentVertex.point():ep->points[0], ep->parentVertex ? ep->parentVertex.point() : ep->points[ep->points.size()-1]);
	gl(BindBuffer, priv->funcs)(GL_ARRAY_BUFFER, priv->pathBuffers[ep->vaoi].vbo);
	gl(BufferData, priv->funcs)(GL_ARRAY_BUFFER, sizeof(PointGL)*pathGL.size(), pathGL.data(), GL_STATIC_DRAW);
	doneCurrent();
}
void Viewer::updateEdge(EdgePriv * ep, Point leftPoint, Point rightPoint)
{
		makeCurrent();
	if(ep->vaoi<=0)
		ep->vaoi=priv->createPbi();
	auto pathGL=priv->pathToPathGL(ep->points, leftPoint, rightPoint);
	gl(BindBuffer, priv->funcs)(GL_ARRAY_BUFFER, priv->pathBuffers[ep->vaoi].vbo);
	gl(BufferData, priv->funcs)(GL_ARRAY_BUFFER, sizeof(PointGL)*pathGL.size(), pathGL.data(), GL_STATIC_DRAW);
	doneCurrent();
}
void Viewer::setProgressData(const std::vector<int32_t>& dat) {
	priv->num_progr=dat.size()/3;
	if(!priv->num_progr)
		return;
	std::vector<GLint> pos(dat.size());
	for(size_t j=0; j<dat.size(); j++) {
		pos[j]=dat[j];
	}
	makeCurrent();
	gl(BindBuffer, priv->funcs)(GL_ARRAY_BUFFER, priv->vbo_progr);
	gl(BufferData, priv->funcs)(GL_ARRAY_BUFFER, pos.size()*sizeof(GLint), pos.data(), GL_STATIC_DRAW);
	doneCurrent();
}
void Viewer::setPathData(const Path& pth) {
	if(pth.points.size()<2) {
		priv->num_path=0;
		return;
	}
	priv->num_path=pth.points.size();
	auto pathGL=priv->pathToPathGL(pth.points,pth.edge0?pth.edge0.points()[pth.index0]: pth.points[0], pth.edge1 ? pth.edge1.points()[pth.index1]:pth.points.back());
	makeCurrent();
	gl(BindBuffer, priv->funcs)(GL_ARRAY_BUFFER, priv->pathBuffers[priv->pbiPath].vbo);
	gl(BufferData, priv->funcs)(GL_ARRAY_BUFFER, sizeof(PointGL)*pathGL.size(), pathGL.data(), GL_STATIC_DRAW);
	doneCurrent();
}
void Viewer::saveOptions(Options* options) const {
	options->setXyz("viewer.direction.up", priv->up.x(), priv->up.y(), priv->up.z());
	options->setXyz("viewer.direction.right", priv->rgt.x(), priv->rgt.y(), priv->rgt.z());
	options->setDouble("viewer.zoom", priv->radU);
	ViewerGeneralOptions::instance()->saveConfig(priv, options);
	ViewerColorOptions::instance()->saveConfig(priv, options);
	ViewerVolumeOptions::instance()->saveConfig(priv, options);
}

void Viewer::attachOptionsDlg() {
	ViewerGeneralOptions::instance()->attach(priv);
	ViewerColorOptions::instance()->attach(priv);
	ViewerVolumeOptions::instance()->attach(priv);
}
void Viewer::detachOptionsDlg() {
	ViewerGeneralOptions::instance()->attach(nullptr);
	ViewerColorOptions::instance()->attach(nullptr);
	ViewerVolumeOptions::instance()->attach(nullptr);
}

void Viewer::updateXfunc(int ch, const std::pair<double, double>& xfunc) {
	priv->updateXfunc(ch, xfunc);
}
void Viewer::updateBoundingBox(const std::array<double, 6>& bbox) {
	priv->updateBoundingBox(bbox);
}

void Viewer::dropEvent(QDropEvent* drop) {
	auto data=drop->mimeData();
	if(data->hasFormat("text/uri-list")) {
		auto urls=data->urls();
		if(urls.size()==1 && urls[0].isLocalFile()) {
			auto file=urls[0].toLocalFile();
			if(file.endsWith(QLatin1String{".txt"}, Qt::CaseInsensitive)) {
				priv->loadColormap(file);
				update();
			} else if(file.endsWith(QLatin1String{".obj"}, Qt::CaseInsensitive)) {
				priv->loadSurface(file);
				update();
			}
		}
	}
	/*
		if(urls.size()==1) {
		QFile f{urls[0].toLocalFile()};
		if(!f.open(QIODevice::ReadOnly))
		throwError("Failed to open");
		QTextStream fs{&f};
		int group;
		std::vector<int> groups{};
		while(true) {
		fs>>group;
		auto stat=fs.status();
		if(stat!=QTextStream::Ok) {
		if(stat==QTextStream::ReadPastEnd)
		break;
		throwError("Failed to read line");
		}
		printMessage("COLOR ", group);
		groups.push_back(group);
		}
		std::swap(priv->colormap, groups);
		for(auto i: groups) {
		printMessage("COLOR ", i);
		}
		update();
		}
		*/
}
void Viewer::dragEnterEvent(QDragEnterEvent* drop) {
	if(drop->mimeData()->hasFormat("text/uri-list")) {
		auto urls=drop->mimeData()->urls();
		if(urls.size()==1 && urls[0].isLocalFile()) {
			auto file=urls[0].toLocalFile();
			if(file.endsWith(QLatin1String{".txt"}, Qt::CaseInsensitive)
					|| file.endsWith(QLatin1String{".obj"}, Qt::CaseInsensitive)) {
				drop->accept();
			}
		}
	}
}

void Viewer::initPainter()
{
	int size = width() > height() ? height() : width();
	m_textImg = new QImage(size/2, size / 2, QImage::Format_RGBA8888);
	m_textImg->fill(0);
	if (!m_Painter) {
		m_Painter = new QPainter;
	}
	m_Painter->begin(m_textImg);
	m_Painter->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
	m_Painter->end();
	//paintText("Fast Neurite Tracer", 1, 20);
	for (auto i = m_Texts.begin(); i != m_Texts.end(); i++)
		paintText(i->second,i->first);
}

void Viewer::paintText(std::string text,int r, int size)
{
	std::map<int, std::string>::iterator iter=m_Texts.find(r);
	if (iter == m_Texts.end())
		m_Texts[r]=text;
	m_Painter->begin(m_textImg);
	QPen pen;
	pen.setColor(Qt::white);
	QFont font;
	font.setFamily("Times New Roman");
	font.setPointSize(size);
	m_Painter->setFont(font);
	m_Painter->setPen(pen);
	m_Painter->fillRect(10,30*r-size,m_textImg->width()-10,size,QColor(0,0,0,255));
	//m_Painter->drawRect(10, 30 * r, m_textImg->width() - 10, size);
	m_Painter->drawText(10, 30*r, QString::fromStdString(text));
	m_Painter->end();
	m_textUpdate = true;
}

QImage Viewer::takeScreenshot() {
	if(!priv->graph)
		return {};

	makeCurrent();
	priv->paintEdge();
	priv->paintOpaque();
	priv->paintSurface();
	priv->paintVolume();
	priv->paintSorted(priv->fbo_scale);
	gl(Finish, priv->funcs)();

	QImage img{priv->fbo_width, priv->fbo_height, QImage::Format_ARGB32};
	gl(BindFramebuffer, priv->funcs)(GL_READ_FRAMEBUFFER, priv->fbo_scale);
	gl(ReadBuffer, priv->funcs)(GL_COLOR_ATTACHMENT0);
	gl(ReadPixels, priv->funcs)(0, 0, priv->fbo_width, priv->fbo_height, GL_BGRA, GL_UNSIGNED_BYTE, img.bits());
	doneCurrent();
	return img.mirrored(false, true);
}

void Viewer::toggleRotate() {
	priv->toggleRotate();
}
void Viewer::setSliceMode() {
	if(priv->graph) {
		priv->slice_mode=!priv->slice_mode;
		priv->slice_delta=0;
		update();
		return;
	}
}
bool Viewer::sliceMode() {
	return priv->slice_mode;
}
bool Viewer::edgeVisibility() {
	return priv->show_edges;
}
void Viewer::setEdgeVisibility() {
	if(priv->graph) {
		priv->show_edges=!priv->show_edges;
		update();
		return;
	}
}
void Viewer::switchChanels() {
	if (priv->graph) {
		if (priv->radU == 1 * priv->max_dimension_size) {
			priv->radU = priv->cubes[0].data.size(0) * priv->min_voxel_size;
		}
		else
			priv->radU = 1 * priv->max_dimension_size;
		priv->updateMVPMatrices();
		update();
		return;
	}
}


#include "viewer.options.cpp"

